/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.servlet;

import java.util.Arrays;
import java.util.stream.Collectors;
import org.apache.solr.client.api.model.ErrorInfo;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.slf4j.Logger;

/** Response helper methods. */
public class ResponseUtils {
  private ResponseUtils() {}

  // System property to use if the Solr core does not exist or solr.hideStackTrace is not
  // configured. (i.e.: a lot of unit test).
  private static final boolean SYSTEM_HIDE_STACK_TRACES = Boolean.getBoolean("solr.hideStackTrace");

  /**
   * Adds the given Throwable's message to the given NamedList.
   *
   * <p>Primarily used by v1 code; v2 endpoints or dispatch code should call {@link
   * #getTypedErrorInfo(Throwable, Logger)}
   *
   * <p>If the response code is not a regular code, the Throwable's stack trace is both logged and
   * added to the given NamedList.
   *
   * <p>Status codes less than 100 are adjusted to be 500.
   *
   * @see #getTypedErrorInfo(Throwable, Logger)
   */
  public static int getErrorInfo(Throwable ex, NamedList<Object> info, Logger log) {
    return getErrorInfo(ex, info, log, false);
  }

  /**
   * Adds the given Throwable's message to the given NamedList.
   *
   * <p>Primarily used by v1 code; v2 endpoints or dispatch code should call {@link
   * #getTypedErrorInfo(Throwable, Logger)}
   *
   * <p>If the response code is not a regular code, the Throwable's stack trace is both logged and
   * added to the given NamedList.
   *
   * <p>Status codes less than 100 are adjusted to be 500.
   *
   * <p>Stack trace will not be output if hideTrace=true OR system property
   * solr.hideStackTrace=true.
   *
   * @see #getTypedErrorInfo(Throwable, Logger)
   */
  public static int getErrorInfo(
      Throwable ex, NamedList<Object> info, Logger log, boolean hideTrace) {
    int code = 500;
    if (ex instanceof SolrException solrExc) {
      code = solrExc.code();
      NamedList<String> errorMetadata = solrExc.getMetadata();
      if (errorMetadata == null) {
        errorMetadata = new SimpleOrderedMap<>();
      }
      errorMetadata.add(ErrorInfo.ERROR_CLASS, ex.getClass().getName());
      errorMetadata.add(
          ErrorInfo.ROOT_ERROR_CLASS, SolrException.getRootCause(ex).getClass().getName());
      info.add("metadata", errorMetadata);
    }

    // For any regular code, don't include the stack trace
    boolean printStackTrace = (code == 500 || code < 100) && !hideStackTrace(hideTrace);
    boolean stackTracePrinted = false;
    NamedList<Object> lastTrace = null;
    Throwable causedBy = ex;
    NamedList<Object> errorInfo = info;
    while (causedBy != null) {
      if (lastTrace != null) {
        lastTrace.add("causedBy", errorInfo);
      }
      errorInfo.add("errorClass", causedBy.getClass().getName());
      if (causedBy instanceof SolrException solrException) {
        errorInfo.add("msg", solrException.getResponseMessage());
        if (solrException.getDetails() != null) {
          errorInfo.add("details", solrException.getDetails());
        }
      } else {
        errorInfo.add("msg", causedBy.getMessage());
      }
      if (printStackTrace) {
        stackTracePrinted = true;
        lastTrace = new SimpleOrderedMap<>();
        errorInfo.add("trace", lastTrace);
        lastTrace.add(
            "stackTrace",
            Arrays.stream(causedBy.getStackTrace())
                .map(StackTraceElement::toString)
                .collect(Collectors.toList()));
        causedBy = causedBy.getCause();
        errorInfo = new SimpleOrderedMap<>();
      } else {
        causedBy = null;
      }
    }

    if (code == 500 || code < 100) {
      log.error("500 Exception", ex);

      // non standard codes have undefined results with various servers
      if (code < 100) {
        log.warn("invalid return code: {}", code);
        code = 500;
      }
    }
    if (!stackTracePrinted && ex != null && ex.getMessage() == null) {
      // Find the first non-null message if we aren't printing the stacktrace
      for (Throwable th = ex; th != null; th = th.getCause()) {
        String msg = th.getMessage();
        if (msg != null) {
          info.add("msg", msg);
          break;
        }
      }
    }

    info.add("code", code);
    return code;
  }

  /**
   * Adds information about the given Throwable to a returned {@link ErrorInfo}
   *
   * <p>Primarily used by v2 API code, which can handle such typed information.
   *
   * <p>Status codes less than 100 are adjusted to be 500.
   *
   * @see #getErrorInfo(Throwable, NamedList, Logger)
   */
  public static ErrorInfo getTypedErrorInfo(Throwable ex, Logger log) {
    return getTypedErrorInfo(ex, log, false);
  }

  /**
   * Adds information about the given Throwable to a returned {@link ErrorInfo}
   *
   * <p>Primarily used by v2 API code, which can handle such typed information.
   *
   * <p>Status codes less than 100 are adjusted to be 500.
   *
   * <p>Stack trace will not be output if hideTrace=true OR system property
   * solr.hideStackTrace=true.
   *
   * @see #getErrorInfo(Throwable, NamedList, Logger)
   */
  public static ErrorInfo getTypedErrorInfo(Throwable ex, Logger log, boolean hideTrace) {
    final ErrorInfo errorInfo = new ErrorInfo();
    int code = 500;
    if (ex instanceof SolrException solrExc) {
      code = solrExc.code();
      errorInfo.metadata = new ErrorInfo.ErrorMetadata();
      errorInfo.metadata.errorClass = ex.getClass().getName();
      errorInfo.metadata.rootErrorClass = SolrException.getRootCause(ex).getClass().getName();
      errorInfo.details = solrExc.getDetails();
      errorInfo.msg = solrExc.getResponseMessage();
    }

    for (Throwable th = ex; errorInfo.msg == null && th != null; th = th.getCause()) {
      errorInfo.msg = th.getMessage();
    }

    // For any regular code, don't include the stack trace
    boolean printStackTrace = (code == 500 || code < 100) && !hideStackTrace(hideTrace);
    errorInfo.errorClass = ex.getClass().getName();
    if (errorInfo.msg == null) {
      errorInfo.msg = ex.getMessage();
    }

    if (printStackTrace) {
      errorInfo.trace = new ErrorInfo.ErrorStackTrace();
      errorInfo.trace.stackTrace =
          Arrays.stream(ex.getStackTrace())
              .map(StackTraceElement::toString)
              .collect(Collectors.toList());

      ErrorInfo.ErrorStackTrace lastTrace = errorInfo.trace;
      Throwable causedBy = ex.getCause();

      while (causedBy != null) {
        ErrorInfo.ErrorCausedBy errorCausedBy = new ErrorInfo.ErrorCausedBy();
        lastTrace.causedBy = errorCausedBy;
        errorCausedBy.errorClass = causedBy.getClass().getName();
        if (ex instanceof SolrException solrExc) {
          errorCausedBy.msg = solrExc.getResponseMessage();
          errorInfo.details = solrExc.getDetails();
        } else {
          errorCausedBy.msg = causedBy.getMessage();
        }
        errorCausedBy.trace = new ErrorInfo.ErrorStackTrace();
        errorCausedBy.trace.stackTrace =
            Arrays.stream(causedBy.getStackTrace())
                .map(StackTraceElement::toString)
                .collect(Collectors.toList());
        lastTrace = errorCausedBy.trace;
        causedBy = causedBy.getCause();
      }
    }

    // non standard codes have undefined results with various servers
    if (code < 100) {
      log.warn("invalid return code: {}", code);
      code = 500;
    }
    if (code == 500) {
      log.error("500 Exception", ex);
    }

    errorInfo.code = code;
    return errorInfo;
  }

  private static boolean hideStackTrace(final boolean hideTrace) {
    return hideTrace || SYSTEM_HIDE_STACK_TRACES;
  }
}
